package edu.gmu;

import javax.swing.tree.MutableTreeNode;
import java.io.*;

import edu.gmu.parser.Token;
import edu.gmu.parser.IDLParserConstants;

import java.util.List;
import java.util.LinkedList;

/**
 * Created by IntelliJ IDEA.
 * User: james
 * Date: 3/9/11
 * Time: 3:59 PM
 * To change this template use File | Settings | File Templates.
 */
public class JavaTarget implements Target
{
    // Things to remember
    private String packageName = null;

    // Where to put generated files
    private String outputDir= null;

    // Current file being generated
    private Writer out = null;

    private int prefix = 0;

    public JavaTarget( String outputDir )
    {
         this.outputDir = outputDir;
    }

    /**
     * Generate defines the hand off from the Syntax Parser to the
     * Code Generator - technically we skipped the Semantic Parser
     * and the IR-Optimization, which is typically not needed by
     * an IDL compiler.
     * @param root
     * @throws IOException
     */
    public void generate( ASTNode root ) throws IOException
    {
        // We know first one is specification - want module
        ASTNode moduleNode = (ASTNode)root.getChildAt(0);

        this.handlePackage(moduleNode);

        List messages = findList( moduleNode, "message_dcl" );
        for( int i = 0; i < messages.size(); i++ )
        {
             ASTNode message = (ASTNode)messages.get(i);
             this.handleMessage(message);
        }
    }

    public void handlePackage( ASTNode moduleNode ) throws IOException
    {
        // No writing here - wait until files are opened then use in handleMessage
        ASTNode scopedName = findNode(moduleNode, "scoped_name");
        this.packageName = getScopedName( scopedName );
    }

    /**
     * Creates file for message, calls to create body, and finally closes file.
     * @param message
     * @throws IOException
     */
    public void handleMessage( ASTNode message ) throws IOException
    {
        ASTNode messageHeader = findNode( message, "message_header" );
        ASTNode messageBody   = findNode( message, "message_body" );
        ASTNode messageId     = findKind( messageHeader, IDLParserConstants.ID );


        //-------------------------------------------------------------------//
        // Open file for writing
        //-------------------------------------------------------------------//
        this.openFile( messageId.getImage() + ".java");

        this.out.write( "package "+this.packageName+";\n" );
        // Check to see if the message extends another
        this.out.write( "public class "+messageId.getImage() );

        boolean doesExtend = false;
        ASTNode extendsNode = findNode( messageHeader, "extends" );
        if( extendsNode != null )
        {
            doesExtend = true;
            ASTNode scopedName = findNode(extendsNode, "scoped_name");
            String extendsName = getScopedName(scopedName);
            this.out.write( " extends "+extendsName );
        }
        // Close class header definition - make sure to add interface
        this.out.write( " implements edu.gmu.middleware.Message{\n" );

        this.handleConstructor(messageId);
        List members = findList( messageBody, "member" );
        for( int i = 0; i < members.size(); i++ )
        {
            ASTNode member = (ASTNode)members.get(i);
            this.handleMember(member);
        }
        this.handleMarshaling(members, doesExtend);

        this.out.write( "}\n" );

        //-------------------------------------------------------------------//
        // Close file - no more writing
        //-------------------------------------------------------------------//
        this.closeFile();
    }



    public void handleConstructor( ASTNode messageId ) throws IOException
    {
         this.out.write( "public "+messageId.getImage()+"() { }\n" );
    }

    public void handleMember( ASTNode member ) throws IOException
    {
        ASTNode id   = (ASTNode)member.getChildAt(0);
        ASTNode type = (ASTNode)member.getChildAt(1);

        this.out.write( "public " );

        // Write out the type - either simple or complex
        if( type.getImage().equals("list") )
        {
            ASTNode scopedName = (ASTNode)type.getChildAt(0);
            this.out.write( "java.util.List <"+getScopedName(scopedName)+">" );
        }
        else if( type.getImage().equals("array") )
        {
             ASTNode arrayType = (ASTNode)type.getChildAt(0);
             String javaType = mapType( arrayType.getImage() );
             this.out.write( javaType+"[]" );
        }
        else // we are a simple_type_spec
        {
            if( type.getImage().equals("scoped_name") )
            {
                String scopedType = getScopedName(type);
                this.out.write( scopedType );
            }
            else // we know it is a base_type_spec
            {
                // Map idl to java
                String javaType = mapType( type.getImage() );
                this.out.write( javaType );
            }
        }

        // Write out the id
        this.out.write( " " + id.getImage()+";\n" );
    }

    public void handleMarshaling( List members, boolean doesExtend ) throws IOException
    {
        StringBuffer output = new StringBuffer();
        StringBuffer input  = new StringBuffer();

        output.append( "\npublic void marshal(edu.gmu.streams.BlockOutputStream output) "+
                             "throws java.io.IOException{\n" );

        input.append(  "\npublic void unmarshal(edu.gmu.streams.BlockInputStream input) "+
                             "throws java.io.IOException{\n" );

        String ind = "    ";

        // If we extend - be sure to call super
        if( doesExtend == true )
        {
            output.append( ind+"super.marshal(output);\n" );
            input.append(  ind+"super.unmarshal(input);\n" );
        }

        for( int i = 0; i < members.size(); i++ )
        {
            ASTNode member = (ASTNode)members.get(i);



            ASTNode id   = (ASTNode)member.getChildAt(0);
            ASTNode type = (ASTNode)member.getChildAt(1);

            // Depending on type
            if( type.getImage().equals("list") )
            {
                ASTNode scopedName = (ASTNode)type.getChildAt(0);
                String  typeName   = getScopedName(scopedName);

                output.append( ind+"int "+id+"Size = "+id+".size();\n" );
                output.append( ind+"output.writeInt( "+id+"Size );\n" );
                output.append( ind+"for( int i = 0; i < "+id+"Size; i++){\n" );
                output.append( ind+"    this."+id+".get(i).marshal(output);\n" );
                output.append( ind+"}\n" );

                input.append( ind+"this."+id+" = new java.util.LinkedList<"+typeName+">();\n" );
                input.append( ind+"int "+id+"Size = input.readInt();\n" );
                input.append( ind+"for( int i = 0; i < "+id+"Size; i++){\n" );
                input.append( ind+"    "+typeName+" aMessage = new "+typeName+"();\n" );
                input.append( ind+"    aMessage.unmarshal(input);\n" );
                input.append( ind+"    this."+id+".add(aMessage);\n" );
                input.append( ind+"}\n" );
            }
            else if( type.getImage().equals("array") )
            {
                ASTNode arrayType = (ASTNode)type.getChildAt(0);
                String javaType = mapType( arrayType.getImage() );

                output.append( ind+"int "+id+"Size = "+id+".length;\n" );
                output.append( ind+"output.writeInt( "+id+"Size );\n" );
                output.append( ind+"output.write( "+id+" );\n");

                input.append( ind+"int "+id+"Size = input.readInt();\n" );
                input.append( ind+"this."+id+" = new "+javaType+"["+id+"Size];\n" );
                input.append( ind+"input.read( "+id+" );\n");
            }
            else // we are a simple_type_spec
            {
                if( type.getImage().equals("scoped_name") )
                {
                    String scopedType = getScopedName(type);
                    output.append( ind+"this."+id+".marshal(output);\n" );
                    input.append( ind+"this."+id+" = new "+scopedType+"();\n" );
                    input.append( ind+"this."+id+".unmarshal(input);\n" );
                }
                else // we know it is a base_type_spec
                {
                    // Map idl to java
                    String javaType = mapType( type.getImage() );
                    String methodType = mapMethod(javaType);

                    output.append( ind+"output.write"+methodType+"("+id+");\n" );
                    input.append( ind+"this."+id+" = input.read"+methodType+"();\n" );

                }
            }
        }

        output.append( "}\n" );
        input.append( "}\n" );
        this.out.write( output.toString() );
        this.out.write( input.toString() );
    }

    public void openFile( String name ) throws IOException
    {
        File file = new File( this.outputDir + "/" + name );
        FileOutputStream fos     = new FileOutputStream(file);
        OutputStreamWriter osw   = new OutputStreamWriter(fos);
        this.out                 = new BufferedWriter( osw );
        //Writer out = new BufferedWriter(new OutputStreamWriter(System.out));
    }

    public void closeFile( ) throws IOException
    {
        this.out.close();
    }

    /**
     * Utility map java types to appropriate method calls on input/output streams.
     * @param javaType
     * @return methodType
     */
    public static String mapMethod( String javaType )
    {
        String methodType = javaType;
        // Some quick and dirty to make object or string valid
        if(      javaType.equals("String") ) methodType = "UTF";
        else if( javaType.equals("int") )    methodType = "Int";
        else if( javaType.equals("long") )   methodType = "Long";
        else if( javaType.equals("float") )  methodType = "Float";
        else if( javaType.equals("double") ) methodType = "Double";
        else if( javaType.equals("char") )   methodType = "Char";
        else if( javaType.equals("byte") )   methodType = "Byte";
        else if( javaType.equals("boolean") )methodType = "Boolean";
        return methodType;
    }

    /**
     * Utility map primitive idl types to java types
     * @param idlType
     * @return javaType
     */
    public static String mapType( String idlType )
    {
        String javaType = idlType;
        // Some quick and dirty to make object or string valid
        if(      idlType.equals("string") ) javaType = "String";
        else if( idlType.equals("object") ) javaType = "Object";
        return javaType;
    }

    /**
     * Utility to get scoped-name as one long string
     * @param scopedName
     * @return s
     */
    public static String getScopedName( ASTNode scopedName )
    {
        int numChildren = scopedName.getChildCount();
        StringBuffer buf = new StringBuffer();
        for( int i = 0; i < numChildren; i++ )
        {
            ASTNode child = (ASTNode)scopedName.getChildAt(i);
            String delim = ".";
            if( i == 0 ) delim = "";
            buf.insert(0, child.getImage() + delim );

        }
        return buf.toString();
    }


    /**
     * Utility to traverse post syntax-parsing-tree
     * @param prefix
     * @param parent
     */
    public static void process( int prefix, ASTNode parent )
    {
        prefix++;
        // Visit parent first - pre-order or post-order, cant rememeber
        visit( prefix, parent );

        int numChildren = parent.getChildCount();
        for( int i = 0; i < numChildren; i++ )
        {
            ASTNode child = (ASTNode)parent.getChildAt(i);
            process( prefix, child );
        }
    }

    /**
     * Visits each node recursively???
     * @param prefix
     * @param node
     */
    public static void visit( int prefix, ASTNode node )
    {
        StringBuffer buf = new StringBuffer();
        for( int i = 0; i < prefix; i++ )
        {
            buf.append("  ");
        }
        System.out.println( ""+ buf + node.toString()  );
    }

    /**
     * Utility that finds first child with specified image - only searches
     * immediate children.
     * <p>
     * I think that this is ugly and inefficient - if Lexer gave us better
     * Token objects could use simple contant/enum ref equality checks.
     * @param node
     * @param tokenImage
     * @return list of childNodes with specified tokenImage or zero size
     */
    public static List findList( ASTNode node, String tokenImage )
    {
        List childNodes = new LinkedList();
        int numChildren = node.getChildCount();
        for( int i = 0; i < numChildren; i++ )
        {
            ASTNode childNode = (ASTNode)node.getChildAt(i);
            Token token = (Token)childNode.getUserObject();

            if( token.image.equals( tokenImage ) )
            {
                childNodes.add( 0,childNode );
            }
        }
        return childNodes;
    }

    /**
     * Utility that finds first child with specified image - only searches
     * immediate children.
     * @param node
     * @param tokenImage
     * @return childNode with specified tokenImage or null if not found
     */
    public static ASTNode findNode( ASTNode node, String tokenImage )
    {
        ASTNode soughtNode = null;
        int numChildren = node.getChildCount();
        for( int i = 0; i < numChildren; i++ )
        {
            ASTNode childNode = (ASTNode)node.getChildAt(i);
            Token token = (Token)childNode.getUserObject();

            if( token.image.equals( tokenImage ) )
            {
                soughtNode = childNode;
                break;
            }
        }
        return soughtNode;
    }

    /**
     * Utility that finds first child with specified kind - only searches
     * immediate children.
     * @param node
     * @param kind
     * @return childNode with specified tokenKind or null if not found
     */
    public static ASTNode findKind( ASTNode node, int kind )
    {
        ASTNode soughtNode = null;
        int numChildren = node.getChildCount();
        for( int i = 0; i < numChildren; i++ )
        {
            ASTNode childNode = (ASTNode)node.getChildAt(i);
            Token token = (Token)childNode.getUserObject();

            if( token.kind == kind )
            {
                soughtNode = childNode;
                break;
            }
        }
        return soughtNode;
    }
}
